// StatNetDriver.cs
//
// Implements HomeUX.Components.StatNetDriver.
//
// See DocFiles\StatNetDriver.htm for driver usage information.
//

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Xml;
using HomeUX.DriverModel;
using HomeUX.Networking;
using HomeUX.Synchronization;
using HomeUX.Utilities;

namespace HomeUX.Components
{

/// <summary>
/// Implements a HomeUX driver for controlling an Enerzone StatNet thermostat network.
/// </summary>
///
public class StatNetDriver : Driver
{
    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private Fields
    //

    /// <summary>
    /// Maps the name of each thermostat specified on the .config file to the corresponding
	/// <r>Thermostat</r> object.
    /// </summary>
    Dictionary<string, Thermostat> _mapNameToThermostat;

    /// <summary>
    /// Maps the 1-based thermostat number of each thermostat to the corresponding
	/// <r>Thermostat</r> object.
    /// </summary>
	Dictionary<int, Thermostat> _mapIdToThermostat;

    /// <summary>
    /// The serial port specification string that defines the virtual or real serial port to open.
    /// </summary>
    string _serialPortSpec;

    /// <summary>
    /// Provides device-specific hardware access.
    /// </summary>
    StatNetHardware _hardware;

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Device Class Overrides & Related Methods
    //

    /// <summary>
    /// Overrides <r>Driver.OnInitialize</r>.
    /// </summary>
    ///
    protected override void OnInitialize()
    {
        // read attributes of the "<Device>" XML element
        _serialPortSpec = Configuration.GetRequiredAttribute("SerialPort");

        // initialize data structures
		_mapNameToThermostat = new Dictionary<string, Thermostat>();
		_mapIdToThermostat = new Dictionary<int, Thermostat>();

        // read attributes of child XML element
        foreach (ConfigurationElement element in Configuration.Children)
        {
            switch (element.Name)
            {

            case "Thermostat":

                // set <thermostat> to the "<Thermostat>" element
                var thermostat = new Thermostat()
                {
                    Id = element.GetRequiredIntAttribute("Id"),
                    Name = element.GetRequiredAttribute("Name"),
                };

                // ensure that <thermostat.Id> hasn't already been taken by a previous
				// "<Thermostat>" element, and add <thermostat> to <_mapIdToThermostat>
                try
                {
                    _mapIdToThermostat.Add(thermostat.Id, thermostat);
                }
                catch (ArgumentException)
                {
                    throw new ConfigurationException(element,
						"Duplicate thermostat ID: {0}",
                        thermostat.Id);
                }

                // ensure that <thermostat.Name> hasn't already been taken by a previous
				// "<Thermostat>" element, and add <thermostat> to <_mapNameToThermostat>
                try
                {
                    _mapNameToThermostat.Add(thermostat.Name, thermostat);
                }
                catch (ArgumentException)
                {
                    throw new ConfigurationException(element,
						"Duplicate thermostat name: {0}",
                        thermostat.Name);
                }
                break;

            }
        }
    }

    /// <summary>
    /// Overrides <r>Driver.OnEnable</r>.
    /// </summary>
    ///
    protected override void OnEnable()
    {
        // enable occurs asynchronously -- queue an StatNetEnableRequest to perform this operation
        QueueRequest(new StatNetEnableRequest() { Driver = this }, false);
    }

    /// <summary>
    /// Overrides <r>Driver.OnDisable</r>.
    /// </summary>
    ///
    protected override void OnDisable()
    {
        // disable occurs synchronously -- perform it now
        CloseHardware();
        DisableCompleted();
    }

    /// <summary>
    /// Overrides <r>Driver.SerializeStateForClient</r>.
    /// </summary>
    ///
    /// <param name="sessionId">See <r>Driver.SerializeStateForClient</r>.</param>
    ///
    /// <param name="writer">See <r>Driver.SerializeStateForClient</r>.</param>
    ///
    public override void SerializeStateForClient(Guid sessionId, XmlWriter writer)
    {
#if false
        // write the "device-name\Thermostats\..." SyncNode updates
        SyncNodeUpdate update;
        foreach (Thermostat thermostat in _mapNameToThermostat.Values)
        {
            // set <path> to the SyncPath "device-name\Thermostats\thermostat-name"
            SyncPath path = SyncPath.Build(Name, "Thermostats", thermostat.Name);

            // write "<path>\Volume" and "<path>\IsMuted"
            SerializeSyncNodeUpdate(writer, thermostat.Name, path, "Volume");
            SerializeSyncNodeUpdate(writer, thermostat.IsMuted, path, "IsMuted");

            // write "<path>\Input" and "<path>\Input 2" (whichever one(s) exist)
            int connectionId = 1;
            foreach (Input input in thermostat.Connections)
            {
				update = CreateConnectionSyncNodeUpdate(thermostat, connectionId, input);
                update.Serialize(writer, false);
                connectionId++;
            }
        }

        // set <inputNamesPath> to "Configuration\device-name\InputNames"
        SyncPath inputNamesPath = SyncPath.Build("Configuration", Name, "InputNames");

        // write "<inputNamesPath>\-"
        SerializeSyncNodeUpdate(writer, "(None)", inputNamesPath, NotConnected);

        // write each "<inputNamesPath>\input-name"
        foreach (Input input in _mapNameToInput.Values)
            SerializeSyncNodeUpdate(writer, input.Name, inputNamesPath, input.Name);

        // set <thermostatNamesPath> to "Configuration\device-name\ThermostatNames"
        SyncPath thermostatNamesPath = SyncPath.Build("Configuration", Name, "ThermostatNames");

        // write each "<thermostatNamesPath>\thermostat-name"
        foreach (Thermostat thermostat in _mapNameToThermostat.Values)
            SerializeSyncNodeUpdate(writer, thermostat.Name, thermostatNamesPath, thermostat.Name);
#endif
    }

    /// <summary>
    /// See <r>Driver.ProcessSyncNodeUpdate</r>.
    /// </summary>
    ///
    /// <param name="originator">See <r>Driver.ProcessSyncNodeUpdate</r>.</param>
    ///
    /// <param name="update">See <r>Driver.ProcessSyncNodeUpdate</r>.</param>
    ///
    /// <param name="replyBuilder">See <r>Driver.ProcessSyncNodeUpdate</r>.</param>
    ///
    public override void ProcessSyncNodeUpdate(Guid originator, SyncNodeUpdate update,
        MessageBuilder replyBuilder)
    {
#if false
        // a valid SyncPath to update is "device-name\top-level-component\..."; switch based on
        // "top-level-component"
        switch (update.Path.MatchRequiredPart(1, "Thermostats"))
        {

        case 0: // "Thermostats"

            ProcessSyncNodeUpdate_Thermostats(originator, update, replyBuilder);
            break;

        }
#endif
    }

#if false
    /// <summary>
    /// Called when a client requests an update to a SyncTree node that's of the form
    /// "device-name\Thermostats\...".
    /// </summary>
    ///
    /// <param name="originator">See <r>Driver.ProcessSyncNodeUpdate</r>.</param>
    ///
    /// <param name="update">See <r>Driver.ProcessSyncNodeUpdate</r>.</param>
    ///
    /// <param name="replyBuilder">See <r>Driver.ProcessSyncNodeUpdate</r>.</param>
    ///
    void ProcessSyncNodeUpdate_Thermostats(Guid originator, SyncNodeUpdate update,
        MessageBuilder replyBuilder)
    {
        // <update.Path> should be: "device-name\Thermostats\thermostat-name\control-name"
        // Example: "StatNet\Thermostats\Living Room NW\Volume"

		// set <thermostatName> to the "thermostat-name" SyncPath component (see above); error if it's not
		// found or if it doesn't refer to an existing thermostat; set <thermostat> to the Thermostat object
        string thermostatName = update.Path.GetRequiredPart(2);
        Thermostat thermostat;
        if (!_mapNameToThermostat.TryGetValue(thermostatName, out thermostat))
            throw new SyncException(update, "Invalid thermostat name \"{0}\"", thermostatName);

		// set <controlName> to the "control-name" SyncPath component (see above); error if it's
		// not found
        string controlName = update.Path.GetRequiredLastPart(3);

		// interpret <controlName>
        int connectionId;
        if (controlName == "Volume")
        {
            // <update.Path> is "device-name\Thermostats\thermostat-name\Volume"
			QueueRequest(new StatNetThermostatVolumeChangeRequest()
			{
				Driver = this,
                Operation = update,
				Originators = new List<Guid>() { originator },
				Thermostat = thermostat,
				NewVolume = LimitVolume(update.GetTypedValue<double>())
			}, true);
        }
        else
        if (controlName == "IsMuted")
        {
            // <update.Path> is "device-name\Thermostats\thermostat-name\IsMuted"
            QueueRequest(new StatNetThermostatIsMutedChangeRequest()
            {
                Driver = this,
                Operation = update,
                Originators = new List<Guid>() { originator },
                Thermostat = thermostat,
                NewIsMuted = update.GetTypedValue<bool>()
            }, true);
        }
        else
        if ((connectionId = ParseConnectionSpec(controlName, update)) > 0)
		{
            // <update.Path> is "device-name\Thermostats\thermostat-name\Input" or
            // <update.Path> is "device-name\Thermostats\thermostat-name\Input 2" etc.
			QueueRequest(new StatNetConnectionRequest()
			{
				Driver = this,
                Operation = update,
                Originators = new List<Guid>() { originator },
				Thermostat = thermostat,
				ConnectionId = connectionId,
				NewInput = ParseInputName(update.GetTypedValue<string>(), update)
			}, true);
		}
        else
        {
            // unknown <controlName>
            throw new SyncException(update,
                "\"{0}\" is not valid -- expecting one of: Volume, IsMuted, {1}",
                controlName, String.Join(", ", GetConnectionSpecs()));
        }
    }
#endif

    /// <summary>
    /// Overrides <r>Driver.MergeRequests</r>.
    /// </summary>
    ///
    /// <param name="futureRequests">See <r>Driver.MergeRequests</r>.</param>
    ///
    protected override void MergeRequests(IList<Driver.Request> futureRequests)
    {
#if false
        // strategy: we only look at the initial set of StatNetThermostatVolumeChangeRequest requests
        // (i.e. stopping at the first one that isn't such a request), and we look for cases
        // where request B is later (within this set) than request A, where A and B change
        // volume for the same thermostat, and in that case we delete A since B overrides it
        while (true)
        {
            // we need at least 2 requests in order to do a merge
            if (futureRequests.Count < 2)
                return;

            // set <first> to the first request; return if it's not an StatNetThermostatVolumeChangeRequest
			// request; note that we'll repeat this (since we're in a loop)
            var first = futureRequests[0] as StatNetThermostatVolumeChangeRequest;
            if (first == null)
                return;

            // <first> is request A (in the comments above); set <other> to request B; if we find
            // B, remove A, but also add the originator (client session ID) of A to B (effectively
            // merging the requests); if we don't find B, stop looking for requests to merge (if
            // there are other requests that could be merged presumably they were already merged
            // during previous calls to ProcessSyncNodeUpdate)
            StatNetThermostatVolumeChangeRequest other = FindRequestToMergeInto(futureRequests, first);
            if (other != null)
            {
                other.Originators.AddRange(first.Originators);
                futureRequests.RemoveAt(0);
            }
            else
                break;
        }
#endif
    }

#if false
    /// <summary>
    /// Assuming the first request in the list of future <r>Driver.Request</r> objects is a
    /// given <r>StatNetThermostatVolumeChangeRequest</r>, this method scans whatever other
    /// <r>StatNetThermostatVolumeChangeRequest</r> requests follow it (i.e. stopping at the first request
    /// that isn't <r>StatNetThermostatVolumeChangeRequest</r>) to find the first such request that refers
    /// to the same thermostat, and then returns that request (since it will override the first
    /// request).  If no such request is found, <n>null</n> is returned.
    /// </summary>
    ///
    /// <param name="futureRequests">The list of future <r>Driver.Request</r> objects, i.e.
    ///     requests that waiting to be processed.</param>
    ///
    /// <param name="first">The first request in <pr>futureRequests</pr>, which is an
    ///     <r>StatNetThermostatVolumeChangeRequest</r></param>
    ///
    StatNetThermostatVolumeChangeRequest FindRequestToMergeInto(IList<Driver.Request> futureRequests,
        StatNetThermostatVolumeChangeRequest first)
    {
        for (int requestIndex = 1; requestIndex < futureRequests.Count; requestIndex++)
        {
            var other = futureRequests[requestIndex] as StatNetThermostatVolumeChangeRequest;
            if (other == null)
                return null;
            if (other.Thermostat == first.Thermostat)
                return other;
        }

        return null;
    }
#endif

	///////////////////////////////////////////////////////////////////////////////////////////////
	// IDisposable Overrides
	//

    /// <summary>
    /// Implements <n>IDisposable.Dispose</n>.
    /// </summary>
    ///
    public override void Dispose()
    {
        if (_hardware != null)
        {
            _hardware.Dispose();
            _hardware = null;
        }
        base.Dispose();
    }

	///////////////////////////////////////////////////////////////////////////////////////////////
	// Other Methods
	//

    /// <summary>
    /// Assuming <r>_hardware</r> is null, this method creates an <r>StatNetHardware</r> object,
	/// used to access StatNet hardware, and sets <r>_hardware</r> to refer to it.
    /// </summary>
	///
    void OpenHardware()
    {
        VirtualSerialPort serialPort = null;
        try
        {
            // set <serialPort> to a new serial port, as specified by <_serialPortSpec>
            serialPort = VirtualSerialPort.GetPort(_serialPortSpec);

            // set <_hardware> to a newly-allocated StatNetHardware object that will use
			// <serialPort> to communicate with the hardware
            _hardware = new StatNetHardware(WorkQueue, serialPort, Name);

            // hook up <_hardware> events
            // IMPORTANT: do corresponding "-=" in CloseHardware
            _hardware.CommandFailed += OnHardwareCommandFailed;
            _hardware.CommandTimedOut += OnHardwareCommandTimedOut;
            _hardware.NotificationsReceived += OnHardwareNotificationReceived;
            _hardware.WarningsReported += OnHardwareWarningsReported;
            // IMPORTANT: do corresponding "-=" in CloseHardware

            // open <_hardware>; this will fail if the serial port cannot be opened
            _hardware.Open();
        }
        finally
        {
            // if <serialPort> was allocated but ownership of it wasn't transferred to <_hardware>,
			// free <serialPort> to prevent a memory leak
            if ((serialPort != null) && (_hardware == null))
            {
                serialPort.Dispose();
                serialPort = null;
            }
        }

    }

    /// <summary>
    /// Assuming <r>_hardware</r> is not null, this method frees it.
    /// </summary>
    ///
    void CloseHardware()
    {
        // disconnect <_hardware> events
        _hardware.CommandFailed -= OnHardwareCommandFailed;
        _hardware.CommandTimedOut -= OnHardwareCommandTimedOut;
        _hardware.NotificationsReceived -= OnHardwareNotificationReceived;
        _hardware.WarningsReported -= OnHardwareWarningsReported;

		// dispose of <_hardware>
        _hardware.Dispose();
        _hardware = null;
    }

    /// <summary>
    /// See <r>StatNetHardware.CommandFailed</r>.
    /// </summary>
    ///
    /// <param name="sender">See <r>StatNetHardware.CommandFailed</r>.</param>
    ///
    /// <param name="e">See <r>StatNetHardware.CommandFailed</r>.</param>
    ///
    public void OnHardwareCommandFailed(object sender, StatNetHardwareCommandFailedEventArgs e)
    {
        // report the failure to clients
        ReportAlert(MessageAlertType.Warning, "\"{0}\" command failed", e.Command);
    }

    /// <summary>
    /// See <r>SimpleSerialHardware{}.CommandTimedOut</r>.
    /// </summary>
    ///
    /// <param name="sender">See <r>SimpleSerialHardware{}.CommandTimedOut</r>.</param>
    ///
    /// <param name="e">See <r>SimpleSerialHardware{}.CommandTimedOut</r>.</param>
    ///
    public void OnHardwareCommandTimedOut(object sender,
        SimpleSerialHardwareCommandTimedOutEventArgs e)
    {
        // report the timeout to clients
        ReportAlert(MessageAlertType.Warning, "\"{0}\" command timed out", e.Command);
    }

    /// <summary>
    /// See <r>StatNetHardware.NotificationsReceived</r>.
    /// </summary>
    ///
    /// <param name="sender">See <r>StatNetHardware.NotificationsReceived</r>.</param>
    ///
    /// <param name="e">See <r>StatNetHardware.NotificationsReceived</r>.</param>
    ///
    public void OnHardwareNotificationReceived(object sender,
		StatNetHardwareNotificationsReceivedEventArgs e)
    {
        // report the notifications to clients
        BroadcastMessageElements(delegate(Guid sessionId, MessageBuilder messageBuilder)
        {
            foreach (string notification in e.Notifications)
            {
                messageBuilder.WriteAlertElement(MessageAlertType.Warning,
                    MessagePrefix + "Notification: {0}", notification);
            }
        });
    }

    /// <summary>
    /// See <r>StatNetHardware.WarningsReported</r>.
    /// </summary>
    ///
    /// <param name="sender">See <r>StatNetHardware.WarningsReported</r>.</param>
    ///
    /// <param name="e">See <r>StatNetHardware.WarningsReported</r>.</param>
    ///
    public void OnHardwareWarningsReported(object sender, StatNetHardwareWarningsReportedEventArgs e)
    {
        foreach (string message in e.Messages)
            ReportAlert(MessageAlertType.Warning, message);
    }

#if false
    /// <summary>
    /// Given a volume level (a <n>double</n>), this method returns the level limited to the
    /// range 0.0 to 1.0, inclusive.
    /// </summary>
    ///
    /// <param name="volume">The given volume level.</param>
    ///
    double LimitVolume(double volume)
    {
        if (volume < 0)
            return 0;
        else
        if (volume > 1)
            return 1;
        else
            return volume;
    }
#endif

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // StatNetRequest and Derived Types
    //

    /// <summary>
    /// A request from a client.
    /// </summary>
    ///
    abstract class StatNetRequest : Driver.Request
    {
        /// <summary>
        /// The driver that owns this request.
        /// </summary>
        protected new StatNetDriver Driver
        {
            [DebuggerStepThrough]
            get
            {
                return (StatNetDriver)base.Driver;
            }
        }
    }

    /// <summary>
    /// A requst to enable the device.
    /// </summary>
    ///
    [DebuggerDisplay("Enable driver")]
    class StatNetEnableRequest : StatNetRequest, Driver.IEnableRequest
    {
        /// <summary>
        /// Overrides <r>StateMachine.Implementation</r>.
        /// </summary>
        ///
        protected override IEnumerable<bool> Implementation()
        {
            // open the hardware
            try
            {
                Driver.OpenHardware();
            }
            catch (VirtualSerialPortException ex)
            {
                Error = Util.FormatExceptionMessage(ex);
                yield break; // end StatNetRequest
            }

#if false
            // query the hardware for its full status: the volume level of thermostats, which inputs
			// are connected to which thermostats, etc.
            var getFullStatus = new ResultHolder<StatNetGetFullStatusReply>(this);
            Driver._hardware.GetFullStatus(getFullStatus.OnDone);
            yield return true; // wait for async reply
            if (getFullStatus.Result == null)
            {
                Error = "GetFullStatus failed";
                yield break; // end StatNetRequest
            }

            // For each thermostat declared in the .config file, we do the following:
            //   -- Get the thermostat level and use it to initialize Thermostat.Volume and Thermostat.IsMuted.
            //   -- Find the which inputs are connected to that thermostat with unity gain (0 dB), and
            //      copy that information to Thermostat.Connections.
            //        -- If any input is connected to the thermostat but not at unity gain, we'll be
            //           disconnecting that input.
            //        -- If more than MaxInputsPerThermostat of the remaining inputs for the thermostat
            //           are connected, we'll be disconnecting them.
            //        -- If any input is not specified in the .config file as being in use, but
            //           <reply> reported that input as being connected to this thermostat, we'll be
            //           disconnecting the input.
            //
            // For each thermostat that's not declared in the .config file as being in use, we'll
            // ignore that thermostat.
            //
            var crosspointsToDisconnect = new List<Crosspoint>();
            foreach (Thermostat thermostat in Driver._mapNameToThermostat.Values)
            {
                // initialize <thermostat.Volume> and <thermostat.IsMuted> based on the thermostat's gain level
                // as reported in <reply>
                GainToVolumeAndMute(getFullStatus.Result.ThermostatGains[thermostat.Id - 1],
					out thermostat.Volume, out thermostat.IsMuted);

                // enumerate inputs to see which inputs are connected to <thermostat>, and make
                // <thermostat.Connections> match the state of the hardware
                int previousConnectionId = 0;
                for (int inputId = 1; inputId <= Driver._channelCount; inputId++)
                {
                    Input input = Driver._mapIdToInput[inputId];
                    if (input != null)
                    {
                        decimal gain =
							getFullStatus.Result.CrosspointGains[inputId - 1][thermostat.Id - 1];
                        if (gain < 0)
                        {
                            // <input> is not connected to <thermostat>
                        }
                        else
                        if (previousConnectionId >= MaxInputsPerThermostat)
                        {
                            // <input> is connected to <thermostat>, but we've already found as many
                            // inputs connected to this thermostat as this driver can handle --
                            // additional inputs need to be disconnected
                            crosspointsToDisconnect.Add(new Crosspoint(inputId, thermostat.Id));
                        }
                        else
                        {
                            // record the fact that <input> is connected to <thermostat>
                            thermostat.Connections[previousConnectionId++] = input;
                        }
                    }
                    else
                    {
                        // input <inputId> is not specified in the config file as being in use;
                        // if <reply> reports it as being connected to this thermostat, we need to
                        // disconnect it
                        decimal gain =
							getFullStatus.Result.CrosspointGains[inputId - 1][thermostat.Id - 1];
                        if (gain >= 0)
                            crosspointsToDisconnect.Add(new Crosspoint(inputId, thermostat.Id));
                    }
                }
            }
#endif
        }
    }

#if false
    /// <summary>
    /// A request to change the volume of an thermostat.
    /// </summary>
    ///
    [DebuggerDisplay("{DebuggerDisplay}")]
	class StatNetThermostatVolumeChangeRequest : StatNetRequest
	{
        /// <summary>
        /// The thermostat that will be affected.
        /// </summary>
		public Thermostat Thermostat;

        /// <summary>
        /// The new value for <r>Thermostat.Volume</r>.
        /// </summary>
		public double NewVolume;

        /// <summary>
        /// Overrides <r>StateMachine.Implementation</r>.
        /// </summary>
        ///
        protected override IEnumerable<bool> Implementation()
        {
            // set the hardware level to <NewVolume>; if the thermostat is muted, there's nothing to
			// tell the hardware at this point -- we'll use <NewVolume> when the thermostat is unmuted
            if (!Thermostat.IsMuted)
            {
                var setThermostatGain = new ResultHolder<StatNetSetThermostatGainReply>(this);
                decimal gain = VolumeToGain(NewVolume);
				Driver.Trace(true, "HW: Thermostat {0} ({1}): Set Volume={2:n2} ({3})",
                    Thermostat.Id, Thermostat.Name, gain, NewVolume);
                Driver._hardware.SetThermostatGain(Thermostat.Id, gain,
                    setThermostatGain.OnDone);
                yield return true; // wait for async reply
                if (setThermostatGain.Result == null)
                {
                    Error = "SetThermostatGain failed";
                    yield break;
                }
            }

            // update driver state
            Thermostat.Volume = NewVolume;
        }

        /// <summary>
        /// Returns a value used by <c>[DebuggerDisplay]</c>, encapsulated to prevent the debugger
        /// from displaying it in quotes.
        /// </summary>
        public object DebuggerDisplay
        {
            get
            {
                return new EncapsulatedString("Change {0} Volume to {1}",
                    Thermostat.DebuggerDisplay, NewVolume);
            }
        }
    }
#endif

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Other Nested Types
    //

    /// <summary>
    /// Represents a single thermostat in a StatNet thermostat network.
    /// </summary>
    ///
    [DebuggerDisplay("{DebuggerDisplay}")]
    class Thermostat
    {
        /// <summary>
        /// The 1-based hardware number of this thermostat.
        /// </summary>
        public int Id;

        /// <summary>
        /// The name of this thermostat.
        /// </summary>
        public string Name;

#if false
        /// <summary>
        /// The thermostat volume level, in the range 0.0 to 1.0 (inclusive).  If <r>IsMuted</r> is
        /// <n>true</n> then the thermostat is silent regardless of the value of <r>Volume</r>.
        /// </summary>
        public double Volume;

        /// <summary>
        /// <n>true</n> if this thermostat is muted, <n>false</n> if not.
        /// </summary>
        public bool IsMuted;
#endif

        /// <summary>
        /// Returns a value used by <c>[DebuggerDisplay]</c>, encapsulated to prevent the debugger
        /// from displaying it in quotes.
        /// </summary>
        public object DebuggerDisplay
        {
            get
            {
                return new EncapsulatedString("Thermostat \"{0}\" (#{1})", Name, Id);
            }
        }
    }
}

}

